/**
 * Copyright 2010 ArcBees Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package com.gwtplatform.annotation.processor;

import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;

import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.TypeMirror;

/**
 * {@link GenerationHelper} is an internal class that provides common routines
 * only used by the annotation processors.
 * 
 * @author Brendan Doherty 
 * @author Florian Sauter
 * @author Stephen Haberman (concept) 
 */
public class GenerationHelper implements Closeable {

  /**
   * Visibility levels of an element.
   * @author Florian Sauter
   */
  public enum Visibility { 
    PUBLIC, PROTECTED, PRIVATE, DEFAULT;
    
    @Override
    public String toString() {
      return this.name().toLowerCase();
    };
  };
  
  private PrintWriter writer;
  
  public GenerationHelper(Writer sourceWriter) {
    initializeSourceWriter(sourceWriter);
  }
  
  @Override
  public void close() {
    writer.close();
  }

  public void generateClassFooter() {
    println("}");
  }
  
  /**
   * Generates a class header. The class name is provided by the reflection
   * class. Pass null to skip the parent class.
   * 
   * <p>
   * <b>Usage:</b>
   * </p>
   * 
   * <pre>
   * <code>
   *  generateClassHeader(Foo.class.getSimpleName(),
   *        HasName.class.getSimpleName,
   *        "MyGenericInterface{@literal <Foo>}"
   *  )
   *  </code>
   * </pre>
   * <p>
   * <b>Generated example:</b>
   * </p>
   * 
   * <pre>
   * <code>public class MyFoo extends Foo implements HasName, MyGenericInterface{@literal <Foo>} {</code></pre>
   * 
   * @param extendedClassName the parent class name
   * @param interfaceNames array of interface names to be implemented
   */
  public void generateClassHeader(String className, String extendedClassName, String... interfaceNames) {
    println();
    print("{0} class ", Visibility.PUBLIC);
    print(className);
    if (extendedClassName != null) {
      print(" extends " + extendedClassName);
    }
    if (interfaceNames != null && interfaceNames.length > 0) {
      print(" implements ");
      String implodedInterfaceString = implode(interfaceNames, ",");
      print(implodedInterfaceString);
    }
    println(" { ");
  }
  
  public void generateConstructorsUsingFields(String simpleClassName, Collection<VariableElement> fieldElements, Collection<VariableElement> optionalFields) {
    // constructor with all fields
    generateConstructorUsingFields(simpleClassName, fieldElements, null);
    
    if (!optionalFields.isEmpty()) {
      // constructor without optional fields
      ArrayList<VariableElement> fields = new ArrayList<VariableElement>();
      fields.addAll(fieldElements);
      fields.removeAll(optionalFields);
      generateConstructorUsingFields(simpleClassName, fields, optionalFields);
    }
  }

  /**
   * Optional field elements will not be listed in the constructor field list but initialized with the default value.
   */
  public void generateConstructorUsingFields(String simpleClassName, Collection<VariableElement> fieldElements, Collection<VariableElement> optionalFieldElements) {
    println();
    print("  {0} {1}(", Visibility.PUBLIC, simpleClassName);
    generateFieldList(fieldElements, true, false);
    println(") {");
    if (optionalFieldElements != null) {
      for (VariableElement optionalField : optionalFieldElements) {
        if (!isPrimitive(optionalField.asType())) {
          generateFieldAssignment(optionalField, null);
        }
      }
    }
    if (fieldElements != null) {
      for (VariableElement fieldElement : fieldElements) {
        generateFieldAssignment(fieldElement, fieldElement.getSimpleName());
      }
    }
    println("  }");
  }
  
  public void generateEmptyConstructor(String simpleClassName, Visibility visibility) {
    println();
    if (visibility != null && visibility != Visibility.DEFAULT) {
      println("  {0} {1}() { }", visibility, simpleClassName);
    } else {
      println("  {1}() { }", simpleClassName);
    }
  }
  
  public void generateEquals(String simpleClassName, Collection<VariableElement> fieldElements) {
    println();
    println("  @Override");
    println("  public boolean equals(Object other) {");
    println("    if (other != null && other.getClass().equals(this.getClass())) {");
    println("          " + simpleClassName + " o = (" + simpleClassName + ") other;");
    println("      return true");
    for (VariableElement fieldElement : fieldElements) {

      TypeMirror type = fieldElement.asType();
      if (type instanceof ArrayType) {
        // && java.util.Arrays.deepEquals(o.banana, this.banana)
        print("          && java.util.Arrays.deepEquals(o.");
        print(fieldElement.getSimpleName());
        print(", this.");
        print(fieldElement.getSimpleName());
        println(")");

      } else if (isPrimitive(type)) {
        // && o.blah == this.blah
        print("          && o.");
        print(fieldElement.getSimpleName());
        print(" == this.");
        println(fieldElement.getSimpleName());
      } else {
        // && ((o.blah == null && this.blah == null) || (o.blah != null &&
        // o.blah.equals(this.blah)))
        print("          && ((o.");
        print(fieldElement.getSimpleName());
        print(" == null && this.");
        print(fieldElement.getSimpleName());
        print(" == null) || (o.");
        print(fieldElement.getSimpleName());
        print(" != null && o.");
        print(fieldElement.getSimpleName());
        print(".equals(this.");
        print(fieldElement.getSimpleName());
        println(")))");
      }
    }
    println("        ;");
    println("    }");
    println("    return false;");
    println("  }");
  }
  
  public void generateFields(Collection<VariableElement> collection, boolean useFinal) {
    println();
    
    for (VariableElement fieldElement : collection) {

      print("  {0} ", Visibility.PRIVATE);
      if (useFinal && !isPrimitive(fieldElement.asType())) {
        print("final ");
      }
      print(fieldElement.asType().toString());
      print(" ");
      print(fieldElement.getSimpleName());
      println(";");
    }
  }

  public void generateHashCode(Collection<VariableElement> fieldElements) {
    println();
    println("  @Override");
    println("  public int hashCode() {");
    println("    int hashCode = 23;");
    println("    hashCode = (hashCode * 37) + getClass().hashCode();");
    for (VariableElement fieldElement : fieldElements) {
      print("    hashCode = (hashCode * 37) + ");
      TypeMirror type = fieldElement.asType();
      if (type instanceof ArrayType) {
        // hashCode = (hashCode * 37) + java.util.Arrays.deepHashCode(banana);
        print("java.util.Arrays.deepHashCode(");
        print(fieldElement.getSimpleName());
        println(");");
      } else if (isPrimitive(fieldElement.asType())) {
        // hashCode = (hashCode * 37) + new Integer(height).hashCode();
        print("new ");
        print(determineWrapperClass(fieldElement.asType()));
        print("(");
        print(fieldElement.getSimpleName());
        println(").hashCode();");

      } else {
        // hashCode = (hashCode * 37) + (blah == null ? 1 : blah.hashCode());
        print("(");
        print(fieldElement.getSimpleName());
        print(" == null ? 1 : ");
        print(fieldElement.getSimpleName());
        println(".hashCode());");
      }
    }
    println("    return hashCode;");
    println("  }");
  }
  
  public void generateFieldAccessors(Collection<VariableElement> fieldElements) {

    for (VariableElement fieldElement : fieldElements) {
      println();
      print("  {0} ", Visibility.PUBLIC);
      print(fieldElement.asType().toString());
      print(" ");
      print(determineAccessorName(fieldElement));
      println("() {");
      print("    return ");
      print(fieldElement.getSimpleName());
      println(";");
      println("  }");
    }
  }
  
  public void generateFieldAssignment(VariableElement field, Object value) {
    println("    this.{0} = {1};", field.getSimpleName(), String.valueOf(value));
  }

  public void generateFieldList(Collection<VariableElement> fieldElements, boolean withType, boolean leadingComma) {
    int i = 0;
    for (VariableElement fieldElement : fieldElements) {
      if (leadingComma || i++ > 0) {
        print(", ");
      }
      if (withType) {
        print(fieldElement.asType().toString());
        print(" ");
      }
      print(fieldElement.getSimpleName());
    }
  }

  /**
   * Use null as import to separate import groups.
   * 
   * <p>
   * <b>Usage:</b>
   * </p>
   * 
   * <pre>
   * <code>generateImports({@link EventHandler}.class, {@link GwtEvent}.class, null, {@link GenEventProcessor}.class)</code></pre>
   * 
   * <b>Generated example:</b>
   * 
   * <pre>
   * <code>
   *  import {@link com.google.gwt.event.shared.EventHandler};
   *  import {@link com.google.gwt.event.shared.EventHandler};
   *  
   *  import {@link com.gwtplatform.annotation.processor.GenEventProcessor};
   * </code></pre>
   * 
   * TODO: It seems as the compiler can't find GWT classes during generation - why?
   * 
   * @param imports array of classes to be imported
   */
  public void generateImports(Class<?>... imports) {
    println();
    for (Class<?> importClass : imports) {
      if (importClass == null) {
        println();
      } else {
        println("import {0};", importClass.getClass().getName());
      }
    }
  }
  
  public void generateImports(String... imports) {
    println();
    for (String importClass : imports) {
      if (importClass == null) {
        println();
      } else {
        println("import {0};", importClass);
      }
    }
  }
  
  /**
   * Generates a package declaration.
   * 
   * <p>
   * <b>Generated example:</b>
   * </p>
   * <code>package com.gwtplatform.annotation.processor;</code>
   */
  public void generatePackageDeclaration(String packageName) {
    println("package {0};", packageName);
  }
  
  public void generateToString(String simpleClassName, Collection<VariableElement> fieldElements) {
    println();
    
    println("  @Override");
    println("  public String toString() {");
    println("    return \"" + simpleClassName + "[\"");
    int i = 0;
    for (VariableElement fieldElement : fieldElements) {
      if (i++ > 0) {
        println("                 + \",\"");
      }
      println("                 + " + fieldElement.getSimpleName());
    }
    println("    + \"]\";");
    println("  }");
  }

  public void print(Object o) {
    writer.print(o);
  }
  
  public void print(String s, Object... parameters) {
    print(replaceParameters(s, parameters));
  }
  
  public void println() {
    writer.println();
  }
  
  public void println(Object o) {
    writer.println(o);
  }
  
  public void println(String s, Object... parameters) {
    println(replaceParameters(s, parameters));
  }
  
  /**
   * Construct a single string from an array of strings, gluing them together
   * with the specified delimiter.
   * 
   * @param segments array of strings
   * @param delimiter character that glues the passed strings together
   * @return imploded and glued list of strings
   */
  public static String implode(String[] segments, String delimiter) {
    String implodedString;
    if (segments.length == 0) {
      implodedString = "";
   } else {
       StringBuffer sb = new StringBuffer();
       sb.append(segments[0]);
       for (int i = 1; i < segments.length; i++) {
         if (segments[i] != null && !segments[i].isEmpty()) {
           sb.append(",");
           sb.append(segments[i]);
         }
      }
      implodedString = sb.toString();
    }
    return implodedString;
  }
  
  /**
   * Replaces each placeholder of this string that matches a parameter index.
   * <p><b>Placeholder format:</b> {int}</p>
   * 
   * <p><b>Usage:</b></p>
   * <pre><code>replaceParameters("{0} int myField = {1};", "private", 20);</code></pre>
   * 
   * @param target the string to be replace.
   * @param parameters the replacement parameters
   * @return the resulting string. 
   *   <p>For example:</p> <code>private int myField = 20;</code>
   */
  public static String replaceParameters(String target , Object... parameters) {
    String result = target;
    if (parameters != null) {
      for (int i = 0; i < parameters.length; i++) {
        result = result.replace("{" + i + "}", String.valueOf(parameters[i]));
      }
    }
    return result;
  }
  
  private String determineAccessorName(VariableElement fieldElement) {
    String name;
    if (fieldElement.asType().toString().equals(java.lang.Boolean.class.getSimpleName().toLowerCase())) {
      name = "is";
    } else {
      name = "get";
    }
    name += fieldElement.getSimpleName().toString().substring(0, 1).toUpperCase();
    name += fieldElement.getSimpleName().toString().substring(1);

    return name;
  }
  
  /**
   * Returns the name of the wrapper class for a primitive class.
   */
  private String determineWrapperClass(TypeMirror type) {
    String typeName = type.toString();
    if (typeName.equals("byte")) {
      return java.lang.Byte.class.getSimpleName();
    }
    if (typeName.equals("short")) {
      return java.lang.Short.class.getSimpleName();
    }
    if (typeName.equals("int")) {
      return java.lang.Integer.class.getSimpleName();
    }
    if (typeName.equals("long")) {
      return java.lang.Long.class.getSimpleName();
    }
    if (typeName.equals("float")) {
      return java.lang.Float.class.getSimpleName();
    }
    if (typeName.equals("double")) {
      return java.lang.Double.class.getSimpleName();
    }
    if (typeName.equals("char")) {
      return java.lang.Character.class.getSimpleName();
    }
    if (typeName.equals("boolean")) {
      return java.lang.Boolean.class.getSimpleName();
    } else {
      return null;
    }
  }
  
  /**
   * Checks if a type is a primitive type.
   */
  public boolean isPrimitive(TypeMirror type) {
    String typeName = type.toString();
    if (typeName.equals("byte")) {
      return true;
    }
    if (typeName.equals("short")) {
      return true;
    }
    if (typeName.equals("int")) {
      return true;
    }
    if (typeName.equals("long")) {
      return true;
    }
    if (typeName.equals("float")) {
      return true;
    }
    if (typeName.equals("double")) {
      return true;
    }
    if (typeName.equals("char")) {
      return true;
    }
    if (typeName.equals("boolean")) {
      return true;
    } 
    return false;    
  }
  
  private void initializeSourceWriter(Writer sourceWriter) {
    BufferedWriter bufferedWriter = new BufferedWriter(sourceWriter);
    writer = new PrintWriter(bufferedWriter);
  }
}
